﻿using System;
using System.Text;
using System.Web;
using System.Threading;
using Microsoft.SharePoint;
using System.Security.Principal;
using System.Collections.Generic;
using Microsoft.SharePoint.WebControls;

namespace Voodoo.SharePoint.ReadUnreadMarks
{
    /// <summary>
    /// this httpmodule takes care of marking document library items Read
    /// when a user reads them from the server it has an async queue that runs on a separate thread 
    /// so that the actual server performance is not impacted dramatically 
    /// </summary>
    public class ReadUnreadHTTPModule : IHttpModule
    {

        private BackgroundDocumentMarkupQueue _Queue;
        private Thread[] _ReadUnreadMarkerThreads = new Thread[0];
        private bool _TrackOfficeDocumentPosts = false;

        public void Init(HttpApplication context)
        {
            int currentWorkerThreads = 0;
            int currentIOCompletionPorts = 0;
            // Get the current settings.
            ThreadPool.GetMinThreads(out currentWorkerThreads, out currentIOCompletionPorts);
            int threadCount = UtilityMethods.DocumentTrackerBackgroundThreadCount();
            context.PreSendRequestContent += new EventHandler(ReadUnread_PreSendRequestContent);
            //context.Error += new EventHandler(ReadUnread_Error);
            if (threadCount < 1) threadCount = 1;
            if (threadCount > 5) threadCount = 5;

  
            //IIS is pretty restrictive and we need some room to run our 
            //threads , so we will up them a bit
            currentWorkerThreads += threadCount;
            currentIOCompletionPorts += threadCount + 1;

            ThreadPool.SetMinThreads(currentWorkerThreads, currentIOCompletionPorts);
            _Queue = new BackgroundDocumentMarkupQueue();
            _TrackOfficeDocumentPosts = UtilityMethods.TrackOfficeDocuments();

            _ReadUnreadMarkerThreads = new Thread[threadCount];
            for (int threadIndex = 0; threadIndex < _ReadUnreadMarkerThreads.Length; threadIndex++)
            {
                _ReadUnreadMarkerThreads[threadIndex] = new Thread(new ThreadStart(this.ReadUnreadMarkerThreadProcess));
                _ReadUnreadMarkerThreads[threadIndex].Start();
            }

        }

        ///// <summary>
        ///// diagnostic dump of app domain errors
        ///// </summary>
        ///// <param name="sender"></param>
        ///// <param name="e"></param>
        //void ReadUnread_Error(object sender, EventArgs e)
        //{
        //    HttpApplication httpApp = (HttpApplication)sender;
        //    if (null != httpApp)
        //    {
        //        HttpContext currentContext = httpApp.Context;
        //        if (null != currentContext)
        //        {
        //            Exception serverError = currentContext.Server.GetLastError();
        //            if (!System.Diagnostics.Debugger.IsAttached)
        //            {
        //                System.Diagnostics.Debugger.Break();
        //            }
        //            if (null != serverError)
        //            {
        //                string exceptionText = GetExceptionText(serverError);
        //                string fileName = "C:\\Error." + DateTime.UtcNow.Ticks.ToString() + ".txt";
        //                try
        //                {
        //                    System.IO.File.WriteAllText(fileName, exceptionText);
        //                }
        //                catch { }
        //            }
        //        }
        //    }
        //}

        static string GetExceptionText(Exception exceptionToBuild)
        {
            var returnValue = new StringBuilder();
            var innerException = exceptionToBuild.InnerException;

            returnValue.AppendLine(exceptionToBuild.Message);
            returnValue.AppendLine(exceptionToBuild.StackTrace);
            while (innerException != null)
            {
                returnValue.AppendLine(innerException.Message);
                returnValue.AppendLine(innerException.StackTrace);
                innerException = innerException.InnerException;
            }
            return returnValue.ToString();
        }

        public void Dispose()
        {
            if (null != _Queue)
            {
                _Queue.Close();
            }

            if (null != _ReadUnreadMarkerThreads)
            {
                for (int threadIndex = 0; threadIndex < _ReadUnreadMarkerThreads.Length; threadIndex++)
                {
                    if (null != _ReadUnreadMarkerThreads[threadIndex])
                    {
                        try
                        {
                            _ReadUnreadMarkerThreads[threadIndex].Abort();
                            _ReadUnreadMarkerThreads[threadIndex].Join();
                            _ReadUnreadMarkerThreads[threadIndex] = null;
                        }
                        catch { }
                    }
                }
                _ReadUnreadMarkerThreads = null;
            }
        }

        void ReadUnread_PreSendRequestContent(object sender, EventArgs e)
        {
            //We only care if this is an authenticated GET on document library  
            //In theory this module is only active if the correct configuration
            //conditions exist so we wont check the WriteDirect or DocumentLibrary
            
            HttpContext webContext = ((HttpApplication)sender).Context;
            if (null != webContext.User)
            {
                if (webContext.User.Identity.IsAuthenticated)
                {
                    string authUserName = webContext.User.Identity.Name;
                    if (authUserName.IndexOf('|') > -1)
                    {
                        string[] nameParts = authUserName.Split('|');
                        authUserName = nameParts[nameParts.GetUpperBound(0)];
                    }
                    if (webContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase))
                    {
                        if (_TrackOfficeDocumentPosts)
                        {
                            ProcessPossibleOfficeWebServiceRequest(webContext, authUserName);
                        }
                    }
                    else if (webContext.Request.HttpMethod.Equals("GET",StringComparison.OrdinalIgnoreCase))
                    {
                        if (webContext.Response.StatusCode == 200 || webContext.Response.StatusCode == 304)
                        {
                          
                            if (webContext.Request.Url.AbsolutePath.StartsWith("/_"))
                            {
                                ProcessPossibleOWAWebAppsRequest(webContext, authUserName);
                            }
                            else
                            {
                                //a "normal" document GET does not have a query string
                                if(0 == webContext.Request.QueryString.Count)
                                {   
                                    //TODO how to more accurately determine this is a document GET 
                                    ProcessDocumentUrl(webContext.Request.Path, authUserName);
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// check an see if its a GET for one of the office web app viewers
        /// </summary>
        void ProcessPossibleOWAWebAppsRequest(HttpContext webContext,   string authUserName)
        {
            Uri requestUrl = webContext.Request.Url;
            string requestPath = requestUrl.AbsolutePath.ToLower();
            if (requestPath.Equals("/_layouts/powerpoint.aspx")
                || requestPath.Equals("/_layouts/wordviewer.aspx")
                || requestPath.Equals("/_layouts/xlviewer.aspx")
                || requestPath.Equals("/_layouts/download.aspx"))
            {
                var queryString = System.Web.HttpUtility.ParseQueryString(requestUrl.Query);
                string documentId = queryString["id"];
                if (null == documentId) documentId = queryString["PresentationId"];
                if (null == documentId) documentId = queryString["SourceUrl"];
                if (!string.IsNullOrEmpty(documentId))
                {
                    string documentUrl = requestUrl.Scheme + "://" + requestUrl.Host;
                    if (!requestUrl.IsDefaultPort) documentUrl += ":" + requestUrl.Port.ToString();
                    documentUrl += documentId;
                    ProcessDocumentUrl(documentUrl, authUserName);
                }
            }
        }


      
        /// <summary>
        /// check the request to see if it is an soap  formated
        /// request to open a document using the office protocol
        /// </summary>
 
        void ProcessPossibleOfficeWebServiceRequest(HttpContext webContext, string authUserName)
        {
            var currentWcfContext = System.ServiceModel.OperationContext.Current;
            if (null != currentWcfContext && null != currentWcfContext.RequestContext)
            {
                string soapAction = webContext.Request.Headers["SOAPAction"];
                if (soapAction.Equals("\"http://schemas.microsoft.com/sharepoint/soap/ICellStorages/ExecuteCellStorageRequest\"", StringComparison.OrdinalIgnoreCase))
                {
                    var wcfMessage = currentWcfContext.RequestContext.RequestMessage;
                    if (null != wcfMessage && !wcfMessage.IsEmpty && !wcfMessage.IsFault)
                    {
                        string documentUrl = ParseRequestUrlFromOfficeSoapXml(wcfMessage.ToString());
                        if (!string.IsNullOrEmpty(documentUrl))
                        {
                            ProcessDocumentUrl(documentUrl, authUserName);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// If we have a valid document Url then queue it for marking
        /// using the current security context to access the document
        /// </summary>
        void ProcessDocumentUrl(string documentUrl, string authUserName)
        {
            if (!string.IsNullOrEmpty(documentUrl))
            {
                SPFile wssFile = null;
                Guid parentSiteId = Guid.Empty;
                Guid parentWebId = Guid.Empty;
                try
                {
                    using (SPSite spSite = new SPSite(documentUrl))
                    {
                        using (SPWeb spWeb = spSite.OpenWeb())
                        {
                            wssFile = spWeb.GetFile(documentUrl);
                            if (wssFile.Exists && wssFile.InDocumentLibrary)
                            {
                                parentSiteId = spSite.ID;
                                parentWebId = spWeb.ID;
                            }
                            else
                            {
                                wssFile = null;
                            }
                        }
                    }
                }
                catch
                {
                    wssFile = null;
                }
                if (null != wssFile )
                {
                    if (null != wssFile.Item)
                    {
                        DocumentItemLocator pathLocator = new DocumentItemLocator();
                        pathLocator.Path = documentUrl;
                        pathLocator.ItemId = wssFile.Item.UniqueId;
                        pathLocator.ListId = wssFile.Item.ParentList.ID;
                        pathLocator.SiteId = parentSiteId;
                        pathLocator.WebId = parentWebId;
                        pathLocator.UserName = authUserName;
                        _Queue.Enqueue(pathLocator);
                    }
                }
            }
        }

        /// <summary>
        /// parse out the <Request Url="xxx" > from the soap envelope
        /// Cant cast it to an office message without reflection
        /// so just check for known text, 
        /// </summary>
        string ParseRequestUrlFromOfficeSoapXml(string soapEnvelope)
        {
            string returnValue = string.Empty;
            if (!string.IsNullOrEmpty(soapEnvelope))
            {
                int posStartTag = soapEnvelope.IndexOf("<Request Url=", StringComparison.OrdinalIgnoreCase);
                if (posStartTag > -1)
                {
                    int posStartUrl = soapEnvelope.IndexOf("\"", posStartTag);
                    if (posStartUrl > posStartTag)
                    {
                        posStartUrl++;
                        int posEndUrl = soapEnvelope.IndexOf("\"", posStartUrl);
                        if (posEndUrl > posStartUrl)
                        {
                            returnValue = soapEnvelope.Substring(posStartUrl, posEndUrl - posStartUrl);
                        }
                    }
                }

            }
            return returnValue;
        }

        /// <summary>
        /// thread process for background thread that marks documents
        /// </summary>
        private void ReadUnreadMarkerThreadProcess()
        {
            bool okDokey = true;
            Thread.Sleep(10);

            while (okDokey)
            {
                try
                {
                    //Dequeue will wait on an item or throw an error
                    //so we dont need any sleeping or stuff here
                    DocumentItemLocator queuedLocator = _Queue.Dequeue();
                    UtilityMethods.MarkItemAsRead(queuedLocator);
                }
                catch (QueueClosedException)
                {
                    okDokey = false;
                }
                catch (ThreadAbortException)
                {
                    okDokey = false;
                }
                catch (Exception otherError)
                {
                    //TODO really should log this someplace
                    System.Diagnostics.Trace.WriteLine(otherError.Message);
                }
            }
        }

    }
}
